home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Software Vault: The Gold Collection
/
Software Vault - The Gold Collection (American Databankers) (1993).ISO
/
cdr52
/
bcsmem.zip
/
ARRAY.DOC
next >
Wrap
Text File
|
1993-05-06
|
39KB
|
914 lines
(10U
Clipper Multi-dimensional Arrays
by Roger Donnay
A. Introduction
B. The Evolution of the Multi-dimensional array
C. Classes of Multi-dimensional arrays
1. Parallel symmetrical arrays
2. Ragged symmetrical arrays
3. Asymmetrical arrays
D. Working with Linked arrays
E. Browsing a Multi-dimensional array
F. Saving and Restoring Multi-dimensional arrays
╔════════════════╗
║ INTRODUCTION ║
╚════════════════╝
In my opinion, the single most powerful new feature of Clipper
5.0 is the multi-dimensional array system. Sometimes referred
to as "ragged arrays", this incredibly versatile data type is
almost unlimited in capability and opens the door to a new
kind of programming just not possible in Summer 87.
This seminar explores the use of ragged arrays from a practical
and theoretical viewpoint and is directed to the Clipper
programmer who is still discovering the power of Clipper 5.0.
I don't usually like to use a lot of code snippets in my
seminars, but teaching multi-dimensional arrays is simplified
by using lots of example code, so feel free to use the code
provided. Much of the code examples are extracted from the
dCLIP libraries.
╔═══════════════════════════════════════════════╗
║ THE EVOLUTION OF THE MULTIDIMENSIONAL ARRAY ║
╚═══════════════════════════════════════════════╝
Clipper Summer 87 supported "single-dimensional" arrays. The
data structure allowed the programmer to store information
in a structure called data "elements". Most languages which
support arrays must first pre-define the length of the array
(number of elements) and the length and type of each element
of the array. This is because memory must be pre-allocated
to allow for storage of data in the array elements. This
memory allocation at compile/link time is a type of "early
binding" to insure that memory will always be available
during the running of the application.
Clipper Summer 87 attempted to improve on this concept by
eliminating the need to pre-define the type and size of each
element of the array so the programmer could change any element
at will. This new kind of "dynamic array" made programming
much easier and more powerful, but the programmer now needed
to consider the effects that this dynamic memory allocation
may have on the use of the memory pool at run-time. Dynamic
arrays that were not properly managed could easily run the
program out of memory. Fortunately, this did not occur often
in Summer 87 because even though any element of the array
could be accessed via one (1) symbol declaration (the array
name), the memory allocation and de-allocation for the elements
was handled on an individual basis.
Example of a single-dimensional array (S87 code):
* make an array of 5 elements
DECLARE myArray[5]
* initialize each element as a null string value
AFILL( myArray, "" )
* make element 2 a date value
myArray[2] = DATE()
* make element 4 a numeric value
myArray[4] = 234.56
The array (myArray) will now look like this:
myArray[1] = ""
myArray[2] = 10/12/92
myArray[3] = ""
myArray[4] = 234.56
myArray[5] = ""
Probably the single most significant advantage to using arrays
in Summer 87 was that any element of an array could be used
in expressions exactly like any memory variable. Another
significant advantage, but maybe not so obvious, was how using
arrays instead of memvars reduced the memory requirements of
the application. This is because an entire array uses only
one symbol, whereas each memory variable uses a separate symbol.
-- Why multi-dimensional arrays? --
Arrays in Summer 87 made the programmer's life much simpler.
Scatter-gather routines, get lists, pick-lists, browse-screens,
menus, etc. became easier to program using arrays because the
size of the array and each element could be determined during
the actual running of the application. Arrays, like databases,
are most effectively used and maintained when they are normalized.
"Normalizing" a database is simply a term describing the careful
structuring of a set of relational databases so that information
can be stored in related groups (ie, invoices, customers, etc.)
with little or no data redundancy. Good programming practices
require that arrays also be structure in sets of related
information. For example, creating an array-driven browse-system
would require the use of many arrays for storing information
like (1) screen-coordinates, (2) field-lists, (3) column-widths,
(4) data-buffers, (5) relations, etc..etc. Summer 87 provided
the ability to create many different arrays, but no easy
mechanism to further relate these sets of arrays to each other.
In the above example, each array had to be given a separate name.
If you wished to save and restore a complete browse configuration,
the process was difficult and impractical and required that the
information be stored and retrieved from databases or kept alive
as PUBLIC arrays and symbols.
I attempted, with a good degree of success, to eliminate the
separately defined arrays in my browse system and tie all the
groups of information together into a single array which was a
"psuedo" multi-dimensional array. I found I could eliminate
the separate arrays and group everything together into one
array by maintaining a group of offset pointers into one long
array. For example, elements 1-4 would be the screen coordinates,
5-55 the field names, 56-106 the column order, etc. A sample of
code that accessed an element of this "psuedo" multi-dimensional
array would look like this:
fieldname = aBrowse[ columns[ columnOffset + currentcol ] ] )
A psuedo-multi-dimensional array would be treated like this:
DECLARE myArray [ 4 + fcount()*2 ]
fieldoffset = 4
columnoffset = 4 + fieldoffset
* put default screen coordinates into array
myArray [1] = 0
myArray [2] = 0
myArray [3] = 24
myArray [4] = 79
* fill field name area of array
for i = 1 to fcount()
myArray[ fieldoffset + i ] = field(i)
next
* fill column pointer area of array with default column widths
for i = 1 to fcount()
myArray[ columnoffset + i ] = len(transform(field(i),'@')
next
Psuedo-multi-dimensional arrays in S87 were possible, but soon
the code started becoming difficult to manage with every line of
code making some reference to an array element "number" and
offset "number" rather than an easy-to-remember symbol "name".
Furthermore, it was impossible to dynamically re-size any
sub-array without completely re-building the entire array.
These limitations made it impractical to do multiple-window
browsing, dynamic adding and deleting of columns, one-to-many
browses, etc.
Regardless of the limitations of the Summer 87 array system, it
was still so powerful that Clipper applications grew in size and
functionality at an alarming rate, thereby creating a whole new
set of problems. The average Clipper programmer didn't want to
invest in a deeper understanding of the memory ramifications of
the heavy use of arrays, so he or she would work around the
problem by using third-party DOS memory managers and overlay
linkers that would give the application more DOS memory to start
with. These third-party products were wonderful and extended
the lifetime of S87, but eventually these applications will
need to be upgraded to a language with a better memory manager.
-- Clipper 5.0 arrays --
Clipper 5.0 looks at arrays in an entirely new light. To
overcome the difficulties that caused us to out-grow the S87
array system, the 5.0 development team needed to treat arrays
not as arrays in the traditional sense but as "linked lists".
The need to grow arrays, shrink arrays, pass parameters in
arrays, return arrays as values, and even store arrays and
code-blocks in other arrays required developing a system to
manage "pointers" and "pointers to pointers". The ultimate
result of this architecture is a system that not only
optimizes the use of symbols, but also the use of memory for
the data elements. This automatic system of "optimizing"
the data elements allows for creating incredibly large arrays
that can be passed around the application for use by many
routines and sub-routines by simply passing pointers rather
than data. This concept vastly improves memory and speed
performance and revolutionizes the future of data-driven
programming.
The development team took this concept even further and went as
far as managing the pointers to the sub-elements so as to simply
create a list of pointers to a "common" memory location for
elements that contained identical data. For example, in Summer
87 or just about any other language, if you create an array of
1000 elements, and initialize each element to a length of 100
characters, a memory block of 100,000 (100*1000) bytes would be
required. Clipper 5.0 effectively handles this task by creating
a system of logical pointers to the same area of memory until
the information in an element is modified. This dynamic
optimization will cause even a huge array to allocate very
little memory when it is created, and from then on, only the
absolute minimum amount needed as the array information changes.
This concept, by itself, is a significant advancement in array
architecture and memory management, but the development team
went even further and created a Virtual Memory Manager (VMM)
system that stores the array data in EMS memory.
To see how effective this array optimization system really is,
try this little test:
for i := 1 to 25
arrayname = 'array'+ALLTRIM(STR(i))
&arrayname = array(1000)
afill(&arrayname,space(1000))
? arrayname, memory(0), memory(4)
next
The above code will create 25 arrays of 1000 elements each and
and 100 characters in each element. This is theoretically a
memory requirement of 25 megabytes. In actuality the above
code when run under dCLIP uses 0 bytes of free pool memory
( MEMORY(0) ) and only 340k of VMM ( MEMORY(4) ).
╔══════════════════════════════════════╗
║ CLASSES OF MULTIDIMENSIONAL ARRAYS ║
╚══════════════════════════════════════╝
Clipper 5.0 provides a variety of methods to create and use
multi-dimensional arrays. I am going to attempt to create
a classification for types of multi-dimensional arrays. These
classes are provided only to help define the different types
of array programming and the advantages or disadvantages of
each. Clipper does not make any distinction and treats all
arrays the same way.
1. Parallel symmetrical arrays.
Parallel arrays are the most common arrays in Clipper
and are usually created using the ARRAY() function to
predefine the length of the array. Parallel is a term
defining the way data elements in the array are "grouped".
An example of parallel arrays would be an array of
3 sub-arrays in which each sub-array has the same number
of elements, and each element in each sub-array has a
direct relationship with the corresponding element in
every other sub-array. For example, a parallel array
containing the structure of a database with 4 fields would
look like this:
Type Contents
aStru[1] A Sub-array of field names
aStru[1,1] C CUST_NAME
aStru[1,2] C SHIP_DATE
aStru[1,3] C BALANCE
aStru[1,4] C PRINT_FLAG
aStru[2] A Sub-array of field types
aStru[2,1] C C
aStru[2,2] C D
aStru[2,3] C N
aStru[2,4] C L
aStru[3] A Sub-array of field lengths
aStru[3,1] C 25
aStru[3,2] C 8
aStru[3,3] C 9
aStru[3,4] C 1
aStru[4] A Sub-array of field decimals
aStru[4,1] N 0
aStru[4,2] N 0
aStru[4,3] N 2
aStru[4,4] N 0
This array could be created like this:
aStru := Array( 4, Fcount() )
FOR i := 1 TO Fcount()
aStru[1,i] := Field(i)
aStru[2,i] := Type(Field(i))
aStru[3,i] := Len(Transform(&(aStru[1,i]),'@'))
aStru[4,i] := Eval( {|n| n := At('.', ;
Transform(&(astru[1,i]),'@')), ;
Iif( n > 0, astru[3,i] - n , 0 ) } )
NEXT
Parallel arrays were very common in Summer 87 code, except
that each array was assigned a different name or symbol,
whereas parallel arrays in 5.0 can all exist under one
array name and are addressed by sub-element number.
Parallel arrays are most commonly used when you need a
pick-list of items (using ACHOICE() or similar function)
and then need to index into a set of parallel arrays to
extract all related information based on a pointer returned
by picking an item from the first parallel array.
2. Ragged symmetrical arrays.
Ragged symmetrical arrays are a new concept introduced in
Clipper 5.0 and are structured in such a way as to make the
data in the array elements much easier to evaluate in a
single expression with the AEVAL() function.
An example of ragged symmetrical arrays would be an array
of 4 sub-arrays in which each sub-array has the same number
of elements (i.e. the same "structure") however, there is
no direct relationship between the information in any one
sub-array and any other sub-array. Instead, all the
pertinent related information is contained in all the
elements of a single sub-array. Let's create a ragged
symmetrical array containing the structure of the same
database we used in the parallel array example above. The
array would look like this:
Type Contents
aStru[1] A Field 1 Information
aStru[1,1] C CUST_NAME
aStru[1,2] C C
aStru[1,3] C 25
aStru[1,4] N 0
aStru[2] A Field 2 Information
aStru[2,1] C SHIP_DATE
aStru[2,2] C D
aStru[2,3] C 8
aStru[2,4] N 0
aStru[3] A Field 3 Information
aStru[3,1] C BALANCE
aStru[3,2] C N
aStru[3,3] C 9
aStru[3,4] N 2
aStru[4] A Field 4 Information
aStru[4,1] C PRINT_FLAG
aStru[4,2] C L
aStru[4,3] C 1
aStru[4, ] N 0
This array could be created like this:
aStru := {}
FOR i := 1 TO Fcount()
Aadd( aStru, Array(4) )
aStru[i,1] := Field(i)
aStru[i,2] := Type(Field(i))
aStru[i,3] := Len(Transform(&(aStru[i,1]),'@'))
aStru[i,4] := Eval( {|n| n := At('.', ;
Transform(&(astru[i,1]),'@')), ;
Iif( n > 0, astru[i,3] - n , 0 ) } )
NEXT
The main advantage of organizing your arrays in ragged
symmetrical form is in the ease of evaluating the
information in the array. For example, listing the
structure of a database that has been loaded into a ragged
symmetrical array would require only a single expression:
Aeval( aStru, { |a| qout(pad(a[1],a[2],a[3],a[4]) } )
Whereas trying to list the same information from parallel
arrays would require more complex structural code:
FOR i := 1 TO Fcount()
qout(pad(aStru[1,i],10),aStru[2,i],aStru[3,i],aStru[4,i])
NEXT
The disadvantage, however of organizing data in ragged
symmetrical form is that the array cannot be simply passed
to a pick-list function such as ACHOICE(), so here's a
handy function to convert one array type to another.
FUNCTION dc_aconvert ( aInput )
LOCAL i, j, aOutput := {}
aOutput := Array( Len( aInput[1] ), Len( aInput ) )
FOR i := 1 TO LEN( aInput )
FOR j := 1 TO LEN( aInput[1] )
aOutput[j,i] := aInput[i,j]
NEXT
NEXT
RETURN aOutput
You would use this function as follows:
aRaggedDir := Directory()
aParallelDir := DC_ACONVERT( aRaggedDir )
nChoice := Achoice( 10,10,20,40, aParallelDir[1] )
? 'The size of '+aParallelDir[1,nChoice]+' is '+ ;
Str( aParallelDir[2,nChoice] )
3. Asymmetrical arrays.
An asymmetrical array is simply an array of information
in which no element of the array has a "direct" indexed or
ordinal relationship with any other element of the array
yet all the information in all the elements make up a
complete package of information. Basically, asymmetrical
arrays can be used in place of memvars, in which each
element of the array represents a specific piece of
information. So why not use memvars instead of arrays?
Mainly, because memvars cannot be easily grouped together
into a complete "set" of information that can be easily
stored and retrieved, whereas and entire array can be
saved, restored, or even passed as a single parameter.
An example of an asymmetrical array would be an array
in which element 1 is another array which always
represents the screen coordinates of a browse screen,
element 2 is a character field with color of the screen,
element 3 is a numeric field representing the work area,
element 4 is a logical field, element 5 is a date field,
etc., etc. In fact an asymmetrical array may even contain
sub-arrays that are ragged symmetrical or parallel
symmetrical. This entire multi-dimensional array could
contain all the information necessary to repaint all
browse screens for all work areas. This kind of array
is effectively an "object", because it encapsulates all
the necessary information about a program configuration
into a nice, neat package.
The advantage of working with large asymmetrical arrays
is that only one symbol is used for the entire array,
making it easy to save, restore and pass around an
application. The disadvantage, is that the programmer
must remember which element of which sub-array contains
the desired data thereby making the source code very
cryptic, unreadable, and difficult to maintain. In
addition, if it became necessary to add new elements to
the array, the ordinal position of each other element may
change. This is where the pre-processor is not only very
handy but an absolute necessity when working with large
asymmetrical arrays. Assigning a "manifest constant" to
an array element now allows the programmer to use multi-
dimensional array elements and sub-elements in the same
way he/she uses memory variables. It isn't necessary to
remember which sub-element of an array contains a piece
of information when a #define statement can be used to
create any name desired. From then on, the programmer
can write or read array information via an easy-to-remember
set of variable names.
There are several schools of thought when #defining
manifest constants to represents array elements. Many
programmers prefer to use the #define "only" for defining
numeric values. Here is an example of this kind of array
pre-processing:
// BROWSE sub-array definitions
#define MAIN 1
#define COORDINATES 2
#define FIELDS 4
// COORDINATES definitions
#define STARTROW 1
#define STARTCOL 2
#define ENDROW 3
#define ENDCOL 4
Defining array manifests in this manner requires that you
use more than 1 variable name to access sub-array
information. For example you would get the value of the
start row of the display like this:
nStartRow := BROWSE[ nWorkArea, COORDINATES, STARTROW ]
Another method of #defining array pointers is by using one
a "manifest symbol" rather than a "manifest constant".
Here is an example of manifest symbols:
// BROWSE sub-array definitions
#define aMAIN 1
#define aCOORDINATES 2
#define aFIELDS 4
// COORDINATES definitions
#define nSTART_ROW BROWSE[ nWorkarea, 2, 1 ]
#define nSTART_COLUMN BROWSE[ nWorkarea, 2, 2 ]
#define nEND_ROW BROWSE[ nWorkarea, 2, 3 ]
#define nEND_COLUMN BROWSE[ nWorkarea, 2, 4 ]
Defining array manifests in this manner allows you to
access multi-dimensional array information via shorter
and simpler commands. For example you would get the value
of the start row of the display like this:
nStartRow := nSTART_ROW
There currently is no "Hungarian notation" standard for
manifest constants so you can use any name you wish, but
I prefer the following format:
xYYYYYYYYYY
- --------
| |
| -------- The assigned name (in all capitals)
------------- The "type" of the data (in lower case)
(a-Array, n-Numeric, d-Date, c-Character, etc)
Defining manifest contstants in this manner makes them stand
out in your program and not get confused with your memory
variables or database field names.
╔══════════════════════════════╗
║ WORKING WITH LINKED ARRAYS ║
╚══════════════════════════════╝
Although array elements in Clipper 5.0 can store the same types
of data as memvars, it must be clearly understood that the 5.0
linked array system creates new "pointers", NOT new "values".
If this truth is forgotten or misunderstood you may not get the
expected results in your program. Let's take the following
code as an example:
n1 := 1
n2 := n1
n2 := 2
? n1
1
a1 := { 1, 2, 3 }
a2 := a1
a2[1] := 10
? a1[1]
10
In the numeric memvar example above, n2 := n1 assigns the "value"
of n1 to n2. There is no other relationship between n1 and n2.
The value of n2 can now be changed to anything without affecting
the value of n1.
In the array example above, a2 := a1 assigns the "address" of
a1 to a2. This now creates a direct relationship between a1 and
a2 by creating a new set of pointers to the same information.
Now if any of the elements of a2 are changed, the information in
a1 will also be changed.
If it desired to copy the information from an array to a new
array and then work with the new array without affecting the
original array, use the ACLONE() function.
Actually, this "linked-list" type of array system is not a
disadvantage, but in the contrary, is the reason why arrays in
Clipper 5.0 are so powerful. When an array is passed as a
parameter to another function or returned as a value from a
function, only the pointer to the same information in memory
is actually passed, not a set of values. This means that
if the data in a LOCAL array that has been passed to a function
is modified, the original passed array is also modified.
Look at the following code:
PROCEDURE TEST
LOCAL a1 := {1,2,3}
LOCAL a2
a2 := TEST2 ( a1 )
? a1[1]
10
STATIC FUNCTION TEST2 ( a3 )
LOCAL a4 := a3
a4[1] := 10
a4[2] := 11
a4[3] := 12
RETURN a4
In the above example, even though a4 in function TEST2 has been
designated as a LOCAL memvar, the assigment of a3 to a4 will
force the information in a1 of procedure TEST to change when
it is changed in function TEST2.
╔═════════════════════════════════════╗
║ BROWSING A MULTIDIMENSIONAL ARRAY ║
╚═════════════════════════════════════╝
I have seen lots of array browsers that don't give me the
dimensional view of the array that I like. Most array browsers
are written using Tbrowse, this one is not. This browser not
only views the information in an array or object but also allows
you to change the value and type of any array element or sub-
element, or to grow or shrink any sub-array. Browsing an array
in this manner requires building a different image of the
array using macros and a Private memvar. This may not be a
desirable programming practice to some, but it yields some good
results. Note: This array browser can take a few seconds to
build the Achoice image of the array if it is a large array.
STATIC nLastKey := 0
MEMVAR aNewArray
FUNCTION abrowse ( aArray )
LOCAL aArrayView, nSelect, nStart, cSaveScreen, cSaveScrn2,;
cArrayElem, xValue, cType, nValueLoc, nRow, cElement, nLength,;
cValue, cOldType, nOldLength, cSubName, nElement, lReBuild,;
nLastElem, getlist := {}
IF !(VALTYPE(aArray)$'AO')
RETURN .f.
ENDIF
PRIVATE aNewArray := aArray
aArrayView := ABROWSE3( {}, aNewArray, '', 0 )
@ 0,2 TO 24,79 DOUBLE
@ 24,7 SAY "┤ ENTER = Edit, ESCape = Exit, + = Add, "+;
"DEL = Delete, INS = Insert ├"
nSelect := 1
nStart := 1
DO WHILE .t.
SET KEY 7 TO ABROWSE4
SET KEY 22 TO ABROWSE4
SET KEY 43 TO ABROWSE4
nLastKey := 0
nSelect := ACHOICE(1,3,23,78,aArrayView,,,nSelect,nStart)
SET KEY 7 TO
SET KEY 22 TO
SET KEY 43 TO
nStart := ROW()-1
IF LASTKEY()=27
EXIT
ENDIF
IF nSelect = 0
LOOP
ENDIF
cArrayElem := aArrayView[nSelect]
cType := SUBSTR( cArrayElem, AT( '],', cArrayElem)+2, 1 )
nValueLoc := AT( '],', cArrayElem ) + 4
cValue := SUBSTR( cArrayElem, nValueLoc )
cElement := SUBSTR( cArrayElem, 1, nValueLoc-4 )
nRow := IIF( ROW()>11, 2, 12 )
cArrayElem := 'aNewArray' + cElement
nLength := LEN(IIF(cType#'A',cValue,&cArrayElem))
lReBuild := .f.
FOR nLastElem := LEN(cElement) TO 1 STEP -1
IF SUBSTR(cElement,nLastElem,1) $ '[,'
EXIT
ENDIF
NEXT
cSubName := 'aNewArray' + ALLTRIM(SUBSTR(cElement,1,nLastElem-1))+;
IIF(SUBSTR(cElement,nLastElem,1)=',',']','')
nElement := VAL(STRTRAN(SUBSTR(cElement,nLastElem+1),']',''))
nOldLength := nLength
DO CASE
CASE nLastKey = 22 // Insert key inserts an element
AINS( &cSubName, nElement)
lReBuild := .t.
CASE nLastKey = 7 // Delete key deletes an element
ADEL( &cSubName, nElement)
lReBuild := .t.
CASE nLastKey = 43 // + key adds a new element
AADD( &cSubName, nil )
lReBuild := .t.
CASE LASTKEY()=13
cSaveScrn2 := SaveScreen( nRow,5,nRow+4,75 )
@ nRow,5 CLEAR TO nRow+4,75
@ nRow,5 TO nRow+4,75
cOldType := cType
@ nRow+1,7 SAY ' Type' GET cType PICT '!' VALID cType$'ACDNLU'
READ
DO CASE
CASE cType = 'L'
xValue := 'T'$cValue
CASE cType = 'D'
xValue := CTOD(cValue)
CASE cType = 'N'
xValue := VAL(cValue)
CASE cType = 'U'
xValue := nil
CASE cType $ 'CA'
@ nRow+2,8 SAY 'Length' GET nLength
READ
IF cType = 'C'
xValue := PAD(cValue,nLength)
ENDIF
ENDCASE
DO CASE
CASE cType = 'N'
@ nRow+3,8 SAY ' Value' GET xValue PICT ;
'9999999999999.9999999'
READ
CASE !cType $ 'UA'
@ nRow+3,8 SAY ' Value' GET xValue PICT '@S58'
READ
ENDCASE
IF LASTKEY()#27
IF cType#'A'
IF cType='U'
aArrayView[nSelect] := cElement + ','+cType+','
ELSE
aArrayView[nSelect] := cElement + ','+cType+','
+TRANSFORM(xValue,'@A')
ENDIF
&cArrayElem := xValue
ELSE
IF TYPE(cArrayElem)#'A'
&cArrayElem := {}
ENDIF
ASIZE(&cArrayElem,nLength)
ENDIF
lReBuild := (cOldType # cType .AND. ;
( cType='A' .OR. cOldType='A' )) ;
.OR. (cType='A' .AND. cOldType='A' ;
.AND. nLength # nOldLength)
ENDIF
RestScreen(nRow,5,nRow+4,75,cSaveScrn2)
ENDCASE
IF lReBuild
aArrayView := ABROWSE3( {}, aNewArray, '', 0 )
ENDIF
ENDDO
DC_IMPL(cSaveScreen)
RETURN .t.
// ----------------------------------------------------------- //
STATIC FUNC aBrowse3 ( aArrayView, aArrayDisp, cElement, nRecurs )
LOCAL nElement, cTextLine, cType, nArrayLen, cValue, cElement2
nArrayLen := LEN(aArrayDisp)
FOR nElement := 1 TO nArrayLen
cType := VALTYPE(aArrayDisp[nElement])
cTextLine := ''
cElement2 := cElement+'['+ALLTRIM(STR(nElement,4))+']'
cElement2 := STRTRAN( cElement2, '][', ',' )
DO CASE
CASE cType='C'
cTextLine := aArrayDisp[nElement]
CASE cType='N'
cTextLine := STR(aArrayDisp[nElement])
CASE cType='D'
cTextLine := DTOC(aArrayDisp[nElement])
CASE cType='L'
cTextLine := IIF(aArrayDisp[nElement],'T','F')
CASE cType$'AO'
AADD( aArrayView, SPACE(nRecurs)+cElement2+','+cType+','+;
cTextLine )
aArrayView := ABROWSE3( aArrayView, aArrayDisp[nElement], ;
cElement2, nRecurs+1 )
LOOP
ENDCASE
AADD( aArrayView, SPACE(nRecurs)+cElement2+','+cType+','+;
cTextLine )
NEXT
RETURN aArrayView
// ---------------------------------------------------------- //
STATIC PROC ABROWSE4
nLastKey := LASTKEY()
KEYBOARD CHR(13)
RETURN
// ---------------------------------------------------------- //
╔═══════════════════════════════╗
║ SAVING AND RESTORING ARRAYS ║
╚═══════════════════════════════╝
Saving and restoring arrays to and from a disk file can offer
the advantage of creating "persitent" objects. The following
code will save and restore complete multi-dimensional arrays
that DO NOT contain code blocks. Currently, there is no
mechanism provided within Clipper to save/restore code blocks
to disk, only to memvars. If you are using code blocks in
arrays, you must store the code block as a string and macro-
compile the string to restore the code block. The code
provided here is 100% clipper code for compatability purposes
only. The array saving/restoring mechanism I prefer is identical
to the below code, except that the F_EOF and F_READLINE functions
are replaced with assembly-level functions for speed.
MEMVAR aOutArray
FUNCTION arrayread ( cFileName )
LOCAL cTextLine, cType, cElement, nArrayLen, nHandle,;
nComma, cValue, cArray
PRIVATE aOutArray := {}
nHandle := FOPEN( cFileName )
IF nHandle<=0
RETURN nil
ENDIF
nArrayLen := 0
DO WHILE !F_EOF(nHandle)
cTextLine := F_READLINE(nHandle)
IF LEFT(cTextLine,1) = '[' .OR. LEFT(cTextLine,1) = ','
nComma := AT(',',cTextLine)
cElement := TRIM(SUBSTR(cTextLine,1,nComma-1))
cType := UPPER(SUBSTR(cTextLine,nComma+1,1))
cValue := SUBSTR(cTextLine,nComma+3)
cArray := 'aOutArray'+cElement
ELSE
cValue := cValue + cTextLine
ENDIF
DO CASE
CASE cType='C'
&cArray := STRTRAN(cValue,CHR(255),CHR(13)+CHR(10))
CASE cType='N'
&cArray := VAL(cValue)
CASE cType='D'
&cArray := CTOD(cValue)
CASE cType='L'
&cArray := IIF(cValue='Y',.t.,.f.)
CASE cType='A'
cArray := 'aOutArray'+cElement
&cArray := {}
ASIZE(&cArray,VAL(cValue))
CASE cType='B'
&cArray := &('"'+cValue+'"')
ENDCASE
ENDDO
FCLOSE(nHandle)
RETURN aOutArray
// ------------------------------------------------------------ //
FUNCTION arraywrite ( aArray, cFileName )
LOCAL cTextLine, cType, nElement, nHandle
IF VALTYPE(aArray)#'A'
RETURN .F.
ENDIF
nHandle := FCREATE( cFileName )
IF nHandle<=0
RETURN .f.
ENDIF
_DCARRAY_W2 ( aArray, nHandle, '' )
FCLOSE(nHandle)
RETURN .T.
// ------------------------------------------------------------- //
STATIC FUNCTION _dcarray_w2 ( aArray , nHandle, cElement )
LOCAL nElement, cTextLine, cType, nArrayLen, cValue
nArrayLen := LEN(aArray)
FWRITE(nHandle,cElement+',A,'+ALLTRIM(STR(nArrayLen))+;
CHR(13)+CHR(10))
FOR nElement := 1 TO nArrayLen
IF VALTYPE(aArray[nElement])='U'
LOOP
ENDIF
cValue := aArray[nElement]
cType := VALTYPE(cValue)
cTextLine := ''
DO CASE
CASE cType='C'
cTextLine := STRTRAN(HARDCR(cValue),CHR(13)+;
CHR(10),CHR(255))
CASE cType='N'
cTextLine := ALLTRIM(STR(cValue))
CASE cType='D'
cTextLine := DTOC(cValue)
CASE cType='L'
cTextLine := IIF(cValue,'Y','N')
CASE cType='A'
_DCARRAY_W2 ( aArray[nElement], nHandle, ;
cElement+'['+ALLTRIM(STR(nElement,4))+']')
LOOP
ENDCASE
FWRITE(nHandle,cElement+'['+ALLTRIM(STR(nElement,4))+']';
+','+cType+','+cTextLine+CHR(13)+CHR(10))
NEXT
RETURN .T.
// -------------------------------------------------------- //
STATIC FUNCTION f_eof ( nHandle )
LOCAL nCurrent, lEof
nCurrent := FSEEK(nHandle,0,1)
lEof := .f.
IF nCurrent >= FSEEK(nHandle,0,2)
lEof := .t.
ENDIF
FSEEK(nHandle,nCurrent,0)
RETURN lEof
// --------------------------------------------------------- //
STATIC FUNCTION f_readline ( nHandle )
LOCAL cString, nCurrent, nCr
nCurrent := FSEEK(nHandle,0,1)
cString := FREADSTR(nHandle,500)
nCr := AT (CHR(13),cString)
FSEEK(nHandle,nCurrent,0)
FSEEK(nHandle,nCr+1,1)
RETURN SUBSTR(cString,1,nCr-1)